// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.api.ads.dfp.lib.utils;

import org.apache.commons.beanutils.PropertyUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Instant;
import org.joda.time.format.DateTimeFormatterBuilder;

import java.lang.reflect.InvocationTargetException;
import java.util.Calendar;
import java.util.Locale;

/**
 * A utility class for handling {@code DateTime} objects.
 *
 * @param <T> the {@code DateTime} type
 * @param <D> the {@code Date} type
 *
 * @author Adam Rogal
 */
public class DateTimesHelper<T, D> {

  private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss";

  private Class<T> dateTimeClass;
  private Class<D> dateClass;

  /**
   * Constructor.
   *
   * @param dateTimeClass the date time class
   * @param dateClass the date class
   */
  public DateTimesHelper(Class<T> dateTimeClass, Class<D> dateClass) {
    this.dateTimeClass = dateTimeClass;
    this.dateClass = dateClass;
  }

  /**
   * Converts a {@code Calendar} object to an API date time preserving the
   * time zone.
   */
  public T toDateTime(Calendar calendar) {
    return toDateTime(new DateTime(calendar));
  }

  /**
   * Converts an {@code Instant} object to an API date time in the time zone
   * supplied.
   */
  public T toDateTime(Instant instant, String timeZoneId) {
    return toDateTime(instant.toDateTime(DateTimeZone.forID(timeZoneId)));
  }

  /**
   * Converts a {@code DateTime} object to an API date time preserving the
   * time zone.
   */
  public T toDateTime(DateTime dateTime) {
    try {
      D dateObj = dateClass.newInstance();
      PropertyUtils.setProperty(dateObj, "year", dateTime.getYear());
      PropertyUtils.setProperty(dateObj, "month", dateTime.getMonthOfYear());
      PropertyUtils.setProperty(dateObj, "day", dateTime.getDayOfMonth());

      T dateTimeObj = dateTimeClass.newInstance();
      PropertyUtils.setProperty(dateTimeObj, "date", dateObj);
      PropertyUtils.setProperty(dateTimeObj, "hour", dateTime.getHourOfDay());
      PropertyUtils.setProperty(dateTimeObj, "minute", dateTime.getMinuteOfHour());
      PropertyUtils.setProperty(dateTimeObj, "second", dateTime.getSecondOfMinute());
      PropertyUtils.setProperty(dateTimeObj, "timeZoneID", dateTime.getZone().toTimeZone().getID());

      return dateTimeObj;
    } catch (InstantiationException e) {
      throw new IllegalStateException("Could not instantiate class.", e);
    } catch (IllegalAccessException e) {
      throw new IllegalStateException("Could not instantiate class.", e);
    } catch (InvocationTargetException e) {
      throw new IllegalStateException("Could not set field.", e);
    } catch (NoSuchMethodException e) {
      throw new IllegalStateException("Could not set field.", e);
    }
  }

  /**
   * Converts a string in the form of {@code yyyy-MM-dd'T'HH:mm:ss} to an API
   * date time in the time zone supplied.
   */
  public T toDateTime(String dateTime, String timeZoneId) {
    return toDateTime(
        new DateTimeFormatterBuilder()
          .appendPattern(DATE_TIME_PATTERN)
          .toFormatter()
          .parseDateTime(dateTime)
          .withMillisOfSecond(0)
          .withZoneRetainFields(DateTimeZone.forID(timeZoneId)));
  }

  /**
   * Converts an API date time to a {@code DateTime} preserving the time zone.
   */
  public DateTime toDateTime(T dateTime) {
    try {
      @SuppressWarnings("unchecked") // Expected class.
      D dateObj = (D) PropertyUtils.getProperty(dateTime, "date");
      return  new DateTime()
          .withYear((Integer) PropertyUtils.getProperty(dateObj, "year"))
          .withMonthOfYear((Integer) PropertyUtils.getProperty(dateObj, "month"))
          .withDayOfMonth((Integer) PropertyUtils.getProperty(dateObj, "day"))
          .withHourOfDay((Integer) PropertyUtils.getProperty(dateTime, "hour"))
          .withMinuteOfHour((Integer) PropertyUtils.getProperty(dateTime, "minute"))
          .withSecondOfMinute((Integer) PropertyUtils.getProperty(dateTime, "second"))
          .withMillisOfSecond(0)
          .withZoneRetainFields(
              DateTimeZone.forID((String) PropertyUtils.getProperty(dateTime, "timeZoneID")));
    } catch (IllegalAccessException e) {
      throw new IllegalStateException("Could not access class.", e);
    } catch (InvocationTargetException e) {
      throw new IllegalStateException("Could not get field.", e);
    } catch (NoSuchMethodException e) {
      throw new IllegalStateException("Could not get field.", e);
    }
  }

  /**
   * Gets a calendar for a {@code DateTime} using the default locale,
   * i.e. Locale.getDefault().
   */
  public Calendar toCalendar(T dateTime) {
    return toDateTime(dateTime).toCalendar(Locale.getDefault());
  }

  /**
   * Gets a calendar for a {@code DateTime} in the supplied locale.
   */
  public Calendar toCalendar(T dateTime, Locale locale) {
    return toDateTime(dateTime).toCalendar(locale);
  }

  /**
   * Returns string representation of this date time. The string representation
   * does not include the time zone since using date times for filtering
   * does not use the time zone. If you need to convert the date time
   * into another time zone before filtering on it, please use
   * {@link #toStringWithZone(Object, String)} instead.
   *
   * @param dateTime the date time to stringify
   * @return a string representation of the {@code DateTime} in
   *         {@code yyyy-MM-dd'T'HH:mm:ss}
   */
  public String toString(T dateTime) {
    return toDateTime(dateTime).toString(DATE_TIME_PATTERN);
  }

  /**
   * Returns string representation of this date time with a different time
   * zone, preserving the millisecond instant.
   * <p>This method is useful for finding the local time in another time zone,
   * especially for filtering.
   * <p>For example, if this date time holds 12:30 in Europe/London, the result
   * from this method with Europe/Paris would be 13:30. You may also want to use
   * this with your network's time zone, i.e.
   * <pre><code> String timeZoneId = networkService.getCurrentNetwork().getTimeZone();
   * String statementPart =
   *     "startDateTime > "
   *         + DateTimes.toString(apiDateTime, timeZoneId);
   * //...
   * statementBuilder.where(statementPart);
   * </code></pre>
   * This method is in the same style of
   * {@link DateTime#withZone(org.joda.time.DateTimeZone)}.
   *
   * @param dateTime the date time to stringify into a new time zone
   * @param newZoneID the time zone ID of the new zone
   * @return a string representation of the {@code DateTime} in
   *         {@code yyyy-MM-dd'T'HH:mm:ss}
   */
  public String toStringWithZone(T dateTime, String newZoneID) {
    return toDateTime(dateTime).withZone(DateTimeZone.forID(newZoneID))
        .toString(DATE_TIME_PATTERN);
  }
}
